PUFFY CORE
WARNING: Only supported by NodeJS >= 13. To use this package with NodeJS 12, use the --experimental-modules
flag.
A collection of ES6 modules with zero-dependencies to help manage common programming tasks in both NodeJS or native JS. This package supports both the import/export
and require
APIs via the exports
and main
entry points in the package.json
.
npm i puffy-core
Table of contents
APIs
collection
CommonJS API: const { collection } = require('puffy-core')
import { batch, uniq, sortBy, seed, headTail, levelUp, flatten, flattenUniq } from 'puffy-core/collection'
console.log(batch([1,2,3,4,5,6,7,8,9,10], 3))
console.log(uniq([1,1,1,1,2,2]))
console.log(uniq([{ name:'Ben' },{ name:'Ben' },{ name:'Jerry' }], x => x.name))
console.log(sortBy([2,1,4,3]))
console.log(sortBy([2,1,4,3], 'desc'))
console.log(sortBy([{ name:'Nic', age:40 }, { name:'Paul', age:50 }, { name:'Lin', age:30 }], x => x.age))
console.log(sortBy([{ name:'Nic', age:40 }, { name:'Paul', age:50 }, { name:'Lin', age:30 }], x => x.age, 'desc'))
console.log(seed(3))
console.log(headTail([1,2,3,4,5,6,7,8], 3))
console.log(levelUp([1], [2,2], [3,3,3]))
console.log(flatten(1,[1,2,3],[4,5,[6,7]],8,9,[6]))
console.log(flattenUniq(1,[1,2,3],[4,5,[6,7]],8,9,[6]))
converter
CommonJS API: const { converter } = require('puffy-core')
import { addZero, nbrToCurrency } from 'puffy-core/converter'
console.log(addZero(123,10))
console.log(nbrToCurrency(123))
console.log(nbrToCurrency(123, '$'))
console.log(nbrToCurrency(123, 'dollar'))
console.log(nbrToCurrency(123, 'euro'))
console.log(nbrToCurrency(123, '€'))
console.log(nbrToCurrency(123, 'yen'))
console.log(nbrToCurrency(123, 'yuan'))
console.log(nbrToCurrency(123, '¥'))
console.log(nbrToCurrency(123, 'pound'))
console.log(nbrToCurrency(123, '£'))
crypto
CommonJS API: const { crypto } = require('puffy-core')
import { encoder, jwtDecode } from 'puffy-core/crypto'
const stringEncoder = encoder()
const stringToBase64 = stringEncoder('base64')
const stringToBin = stringEncoder('bin')
const stringToHex = stringEncoder('hex')
const stringToBuffer = stringEncoder('buffer')
const base64Encoder = encoder('base64')
const base64ToString = base64Encoder()
const base64ToBin = base64Encoder('bin')
const base64ToHex = base64Encoder('hex')
const base64ToBuffer = base64Encoder('buffer')
const binEncoder = encoder('bin')
const binToString = binEncoder()
const binToBase64 = binEncoder('base64')
const binToHex = binEncoder('hex')
const binToBuffer = binEncoder('buffer')
const bufferEncoder = encoder('buffer')
const bufferToString = bufferEncoder()
const bufferToBase64 = bufferEncoder('base64')
const bufferToHex = bufferEncoder('hex')
const bufferToBin = bufferEncoder('bin')
const str = 'hello world'
console.log(stringToBase64(str))
console.log(stringToBin(str))
console.log(stringToHex(str))
console.log(stringToBuffer(str))
const base64Str = 'aGVsbG8gd29ybGQ='
console.log(base64ToString(base64Str))
console.log(base64ToBin(base64Str))
console.log(base64ToHex(base64Str))
console.log(base64ToBuffer(base64Str))
const binStr = '0110100001100101011011000110110001101111001000000111011101101111011100100110110001100100'
console.log(binToString(binStr))
console.log(binToBase64(binStr))
console.log(binToHex(binStr))
console.log(binToBuffer(binStr))
const buf = stringToBuffer('hello world')
console.log(bufferToString(buf))
console.log(bufferToBase64(buf))
console.log(bufferToHex(buf))
console.log(bufferToBin(buf))
const { header, payload, signBase64 } = jwtDecode('YOUR JWT TOKEN HERE')
const data = jwtDecode('YOUR JWT TOKEN HERE', { noFail:true })
date
CommonJS API: const { date } = require('puffy-core')
import { addSeconds, addMinutes, addHours, addDays, addMonths, addYears, formatDate } from 'puffy-core/date'
console.log(addSeconds(new Date()))
console.log(addMinutes(new Date()))
console.log(addHours(new Date()))
console.log(addDays(new Date()))
console.log(addMonths(new Date()))
console.log(addYears(new Date()))
const refDate = new Date('2021-10-12T13:45:21')
console.log(formatDate(refDate))
console.log(formatDate(refDate, { format:'dd-MM-yyyy' }))
console.log(formatDate(refDate, { format:'ddMMyyyy' }))
console.log(formatDate(refDate, { format:'MMyy' }))
console.log(formatDate(refDate, { format:'dd/MM/yy' }))
console.log(formatDate(refDate, { format:'dd/MM/yy HH:mm:ss' }))
console.log(formatDate(refDate, { format:'dd/MM/yy HH:mm:ss', utc:true }))
console.log(formatDate(refDate, { format:'The dd of MMM, yyyy' }))
console.log(formatDate(refDate, { format:'The dd{nth} of MMM, yyyy' }))
error
error
API - Quick start
CommonJS API: const { error } = require('puffy-core')
This API uses a functional approach to handling errors. Instead of throwing errors, errors are accumulated. It is up to the software engineer to manage what to do with them. The Errors are accumulated in the inverse order of occurance. This means that the first error is the highest in the stack while the last error is at the bottom of the stack (i.e., the original error that occured in the first place).
import { catchErrors, wrapErrors, wrapErrorsFn, wrapCustomErrors, mergeErrors, getErrorMetadata, PuffyResponse } from 'puffy-core/error'
const asyncSucceed = () => new Promise(next => next(123))
const asyncFail = () => new Promise((_,fail) => fail(new Error('Boom')))
const meta = { hello:'world' }
const asyncFailWithMetadata = () => new Promise((_,fail) => fail(wrapCustomErrors(meta)('Boom')))
const syncSucceed = () => 123
const syncFail = () => { throw new Error('Boom') }
const main = async options => {
const allErrors = []
const [errors01, data01] = await catchErrors(asyncSucceed())
if (errors01) {
console.log(`'asyncSucceed' failed`)
allErrors.push(...errors01)
} else
console.log(`'asyncSucceed' succeeded`)
const [errors02, data02] = await catchErrors(asyncFail())
if (errors02) {
console.log(`'asyncFail' failed`)
allErrors.push(...errors02)
} else
console.log(`'asyncFail' succeeded`)
const [errors03, data03] = catchErrors(syncSucceed)
if (errors03) {
console.log(`'syncSucceed' failed`)
allErrors.push(...errors03)
} else
console.log(`'syncSucceed' succeeded`)
const [errors04, data04] = catchErrors(syncFail)
if (errors04) {
console.log(`'syncFail' failed`)
allErrors.push(...errors04)
} else
console.log(`'syncFail' succeeded`)
const [errors05, data05] = await catchErrors(asyncFailWithMetadata())
if (errors05) {
console.log('Shows error metadata')
console.log([errors05[0].metadata])
console.log(getErrorMetadata(errors05))
allErrors.push(...errors05)
}
if (allErrors.length) {
const e = wrapErrorsFn(`This API broke`)
if (options && options.wrapErrors)
throw e(allErrors)
else
throw wrapErrors('A few errors occured', allErrors)
} else
return 'no errors'
}
catchErrors(main()).then(([errors, data]) => {
if (errors)
console.error(mergeErrors(errors).message)
else
console.log(data)
})
NOTES:
wrapErrors
supports an arbitrary amount of inputs with both string and Error types:
wrapErrors('I am an error', new Error('I am another error'), [new Error('We are other errors')])
wrapErrorsFn
is sugarcode for (...errors) => wrapErrors(
This API broke, ...errors)
.
Adding a response even when an error is thrown
In this scenario, a process may partially succeed. In those types of cases, we want to capture both the errors and the completed responses. Such process (in the example below myProcess
) would behave like this:
const [errors, resp] = await myProcess(allItems)
if (errors)
manageErrors(errors)
if (resp)
manageResponse(resp)
By default, using the catchErrors
in conjunction with one of the wrapping error APIs (i.e., wrapErrors
, wrapErrorsFn
, wrapCustomErrors
) does not behave as such. By default, the behavior is either fully succeeded or fully failed. To change that behavior, all the wrapping error APIs support a response
API that can pass a response back. For example:
import { catchErrors, wrapErrors } from 'puffy-core/error'
const myProcess = items => catchErrors((async () => {
if (errors)
throw wrapErrors(`Some items have failed`, errors).response(processedItems)
else
return processedItems
})())
func
CommonJS API: const { func } = require('puffy-core')
import { chainAsync, chainSync } from 'puffy-core/func'
import { catchErrors } from 'puffy-core/error'
const main = async () => {
const [errors01, data01] = await chainAsync(
() => Promise.resolve(1),
previous => Promise.resolve(previous+1),
previous => previous+2,
previous => catchErrors(Promise.resolve(previous+1)),
previous => previous+3
)
console.log(errors01)
console.log(data01)
const [errors02, data02] = await chainAsync(
() => Promise.resolve(1),
previous => Promise.resolve(previous+1).then(() => { throw new Error('Boom')})
)
console.log(errors02)
console.log(data02)
const [errors03, data03] = await chainAsync(
() => Promise.resolve(1),
previous => catchErrors(Promise.resolve(previous+1).then(() => { throw new Error('Boom')}))
)
console.log(errors03)
console.log(data03)
const [errors04, data04] = chainSync(
() => 1,
previous => previous+1,
previous => previous+2
)
console.log(errors04)
console.log(data04)
}
main()
math
CommonJS API: const { math } = require('puffy-core')
import { avg, stdDev, median, percentile, getRandomNumber, getRandomNumbers } from 'puffy-core/math'
console.log(avg([5,5,5,5,5]))
console.log(avg([1,2,3,4]))
console.log(stdDev([5,5,5,5,5]))
console.log(stdDev([1,2,3,4]))
console.log(median([5,5,5,5,5]))
console.log(median([1,2,3,4]))
const percentile5th = percentile(5)
const percentile75th = percentile(75)
const percentile95th = percentile(95)
const data = [12,45,23,87,13,54,23,12,1,1,23,67,54,34,35,43,27,56]
console.log(percentile5th(data))
console.log(percentile75th(data))
console.log(percentile95th(data))
console.log(getRandomNumber())
console.log(getRandomNumber({ start:1000, end:3000 }))
console.log(getRandomNumbers({ start:1000, end:3000, size:5 }))
obj
CommonJS API: const { obj } = require('puffy-core')
import { merge, setProperty, getProperty, extractFlattenedJSON, exists, existsAny, existsAll, isEmpty } from 'puffy-core/obj'
console.log(merge(
{ name:'Nic', age:38 },
{ address:{ street:'hello' } },
{ friends:['Peter','Roger'], address:{ code:2000 }, age:40 }))
console.log(setProperty({ name:'Nic' }, 'company.name', 'Neap Pty Ltd'))
console.log(getProperty({ name:'Nic', friends:[{ name:'Peter' },{ name:'Bob' }] }, 'name'))
console.log(getProperty({ name:'Nic', friends:[{ name:'Peter' },{ name:'Bob' }] }, 'friends[1].name'))
console.log(extractFlattenedJSON({
'user.firstName': 'Nicolas',
'user.age': 38,
'user.friends[0].name': 'Brendan',
'user.friends[0].age': 31,
'user.friends[1].name': 'Boris',
'user.friends[1].age': 32
}))
console.log(exists())
console.log(exists(undefined))
console.log(exists(null))
console.log(exists({}))
console.log(exists(0))
console.log(exists('Hello'))
console.log(existsAny(null, undefined))
console.log(existsAny(0, undefined))
console.log(existsAll(null, undefined))
console.log(existsAll(0, undefined))
console.log(existsAll(0, ''))
console.log(isEmpty())
console.log(isEmpty({}))
console.log(isEmpty({ hello:'world' }))
string
CommonJS API: const { string } = require('puffy-core')
import { plural, justifyLeft } from 'puffy-core/string'
console.log(plural(1, 'cat'))
console.log(plural(2, 'cat'))
console.log(plural(2, 'He'))
console.log(plural(2, 'its'))
console.log(plural(1, 'Test record'))
console.log(plural(2, 'Test record'))
console.log(plural(1, 'Project', 'was'))
console.log(plural(2, 'Project', 'was'))
console.log(plural(1, 'She', 'is'))
console.log(plural(2, 'She', 'is'))
console.log(plural(1, 'Test record', 'is'))
console.log(plural(2, 'Test record', 'is'))
const text = ` Hello,
This is a list:
- Fruits:
- Apple
- Orange`
console.log(justifyLeft(text))
console.log(justifyLeft(text, { prefix:'> ' }))
console.log(justifyLeft(text, { anchorLine:0 }))
console.log(justifyLeft(text, { remove:' ' }))
console.log(justifyLeft(text, { remove:true, anchorLine:1 }))
console.log(justifyLeft(text, { remove:true, anchorLine:1, skip:0 }))
console.log(justifyLeft(text, { remove:true, anchorLine:1, skip:[0,1] }))
time
CommonJS API: const { time } = require('puffy-core')
import { delay, Timer } from 'puffy-core/time'
const main = async () => {
const start = Date.now()
await delay(2000)
console.log(`${Date.now() - start} milliseconds have passed.`)
const timer = new Timer()
await delay([1000, 5000])
console.log(`${timer.time('second')} seconds have passed.`)
timer.reStart()
const r = await delay(1000, { hello:'world' })
console.log(r)
const race1 = delay(5000, 'race1')
const race2 = delay(3000, 'race2')
const winner = await Promise.race([race1, race2])
if (winner == 'race1')
race2.cancel()
else
race1.cancel()
}
main()
url
import { getUrlParts } from 'puffy-core/url'
console.log(getUrlParts('https://localhost:3456/hello/world?name=carl&age=40#home'))
validate
CommonJS API: const { validate } = require('puffy-core')
import { validateUrl, validateEmail } from 'puffy-core/validate'
console.log(validateUrl('https://neap.co'))
console.log(validateUrl('hello'))
console.log(validateEmail('nic@neap.co'))
console.log(validateEmail('nic @neap.co'))
Dev
About this project
This project is built using ES6 modules located under the src
folder. All the entry point definitions are located in the package.json
under the exports
property.
rollup
is used to compile the ES6 modules to CommonJS.
Building this project for both CommonJS and ES6 modules
npm run build
This command compiles the ES6 modules located under the src
folder to .cjs
file sent to the dist
folder.
Unit test
npm test
Annexes
Creating URLs with the URL
object
The URL
is a native object supported in both browser and NodeJS environments:
const u = new URL('https://example.com')
console.log(u.toString())
u.pathname = 'blog'
console.log(u.toString())
u.searchParams.set('hello', 'world')
console.log(u.toString())
u.hash = 'footer'
console.log(u.toString())
License
BSD 3-Clause License
Copyright (c) 2019-2021, Cloudless Consulting Pty Ltd
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
-
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
-
Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.